// Copyright DustedPixels.com 2008. All rights reserved.

package com.dustedpixels.jasmin.unit.ula;

import com.dustedpixels.jasmin.unit.Unit;
import com.dustedpixels.jasmin.unit.common.Clock;
import com.dustedpixels.jasmin.unit.common.ClockDivider;
import com.dustedpixels.jasmin.unit.counter.Counter;
import com.dustedpixels.jasmin.unit.memory.ROM;
import com.dustedpixels.jasmin.unit.tools.Oscilloscope;
import com.dustedpixels.jasmin.unit.tools.Pin;
import com.dustedpixels.jasmin.unit.video.RowColCounter;
import com.dustedpixels.jasmin.unit.video.VideoDecoder;
import com.dustedpixels.jasmin.unit.z80.v1.Z80;
import com.dustedpixels.jasmin.video.NullVideoOutput;
import com.dustedpixels.jasmin.video.VideoOutput;

/**
 * @author micapolos@gmail.com (Michal Pociecha-Los)
 */
public final class ULA implements Unit {
  // (output) output clock source for Z80
  public boolean CLOCK = false;
  
  // (output) IRQ
  public boolean IRQ = false;
  
  // Memory control
  public int DADDR = 0;
  public int DADDR_Z = 0;
  public int DATA = 0;
  public int DATA_Z = 0;
  public boolean DMREQ = false;
  public boolean DMREQ_Z = true;
  
  public boolean MREQ;
  public boolean IORQ;
  public boolean WR;
  public boolean RD;
  
  public boolean ROMSEL = false; 
  public boolean DWR = false;
  
  private final Clock masterClock;
  private final ClockDivider clockDivider;
  private final IRQGenerator irqGenerator;

  private final ROM paletteROM;
  private final VideoDecoder videoDecoder;
  private final Counter frameCounter;
  private final RowColCounter rowColCounter;

  private final MemoryDecoder memoryDecoder;
  private final ColorDecoder colorDecoder;

  private final ClockControl clockControl;
  
  public ULA(VideoOutput videoOutput) {
    // Instantiate clocks
    this.masterClock = new Clock();
    this.clockDivider = new ClockDivider();
    
    // Initialize interrupt generator
    this.irqGenerator = new IRQGenerator();
    
    // Initialize counter which will count 3.5MHz cycles.
    // The counter will be reset when IRQ occurs.
    this.frameCounter = new Counter();
    
    // Initialize row and column counter.
    this.rowColCounter = new RowColCounter(312, 448, 296, 48);

    this.memoryDecoder = new MemoryDecoder();
    this.colorDecoder = new ColorDecoder();
    
    // Initialize video decoding
    this.paletteROM = new ROM(4, Constants.PALETTE_RGB);
    this.videoDecoder = new VideoDecoder(videoOutput, 312, 448, 288, 352, 296, 40);
    
    // Initialize Z80 clock control
    this.clockControl = new ClockControl();    
  }
  
  public void updateEndPoints() {
    // Update master clock
    masterClock.updateEndPoints();
    
    // Divide master clock
    clockDivider.IN = masterClock.OUT;
    clockDivider.updateEndPoints();
    
    // Update IRQ
    irqGenerator.CLOCK = clockDivider.OUT;
    irqGenerator.updateEndPoints();
    IRQ = irqGenerator.IRQ;
    
    // Update frame counter every IRQ.
    frameCounter.CLK = irqGenerator.IRQ;
    frameCounter.updateEndPoints();
 
    // Update row & column counter
    rowColCounter.CLOCK = masterClock.OUT;
    rowColCounter.updateEndPoints();
    
    // Decode video memory onto screen/attribute pair
    memoryDecoder.CLOCK = masterClock.OUT;
    memoryDecoder.ROW = rowColCounter.ROW;
    memoryDecoder.COL = rowColCounter.COL;
    memoryDecoder.DATA = DATA;
    memoryDecoder.updateEndPoints();
    
    // Update memory pins
    DADDR = memoryDecoder.ADDR;
    DADDR_Z = memoryDecoder.ADDR_Z;
    DMREQ = memoryDecoder.MREQ;
    DMREQ_Z = memoryDecoder.MREQ_Z;
    
    // Update color decoder
    colorDecoder.CLOCK = masterClock.OUT;
    colorDecoder.ROW = rowColCounter.ROW;
    colorDecoder.COL = rowColCounter.COL;
    colorDecoder.SCREEN = memoryDecoder.SCREEN;
    colorDecoder.ATTR = memoryDecoder.ATTR;
    colorDecoder.BORDER = ((rowColCounter.ROW + (frameCounter.OUT << 1)) % 20) < 10 ? 1 : 6; // TODO
    colorDecoder.FLASH = (frameCounter.OUT & 0x10) == 0;
    colorDecoder.updateEndPoints();
    
    // Update palette ROM
    paletteROM.ADDR = colorDecoder.COLOR;
    paletteROM.RD = true;
    paletteROM.updateEndPoints();
    
    // Update video decoder;
    videoDecoder.CLOCK = masterClock.OUT;
    videoDecoder.RGB = paletteROM.DATA;
    videoDecoder.updateEndPoints();

    // Update Z80 clock (with contention)
    clockControl.CLK_IN = clockDivider.OUT;
    clockControl.ADDR = DADDR;
    clockControl.CONT = memoryDecoder.CONTENTION;
    clockControl.IORQ = IORQ;
    clockControl.MREQ = MREQ;
    clockControl.updateEndPoints();

    // Update ROM selection pin
    ROMSEL = ((DADDR & 0xC000) == 0x0000);
    DWR = ((DADDR & 0xC000) == 0x4000) & WR;
    
    // Update output clock which is driving Z80
    CLOCK = clockControl.CLK_OUT;    
  }
    
  public static void main(String[] args) {
    VideoOutput videoOutput = new NullVideoOutput();
    ULA ula = new ULA(videoOutput);
    
    Oscilloscope oscilloscope = new Oscilloscope(
        Pin.of("CLK7", ula.masterClock, "OUT"),
        Pin.of("CLK35", ula.clockDivider, "OUT"),
        Pin.of("IRQ", ula, "IRQ"),
        Pin.of("CLKZ80", ula, "CLOCK")
    );
    
    for (int i = 0; i < 200; i++) {
      ula.updateEndPoints();
      oscilloscope.sample();
    }
    
    oscilloscope.print(8);
  }
}
